﻿// Copyright 2013 Ronald Schlenker, www.technewlogic.de.
// 
// This file is part of Byond.
// 
// Byond is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
// 
// Byond is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with Byond.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Linq;
using Byond.AudioMidi;
using Byond.Logging;
using Byond.Messages;
using Byond.Midi;
using MidiDotNet;
using Utils.Messaging;
using Channel = MidiDotNet.Channel;
using Control = MidiDotNet.Control;
using NoteOnMessage = Byond.Midi.NoteOnMessage;
using NoteOffMessage = Byond.Midi.NoteOffMessage;
using ControlChangeMessage = Byond.Midi.ControlChangeMessage;
using Pitch = MidiDotNet.Pitch;

namespace Byond.Standalone.MidiDotNetStuff
{
	public class DotNetMidi : MidiBase, IDisposable
	{
		public override event Action<MidiMessage> OnMessage;
		public event MessageEventHandler SendMessage;

		private readonly IUserLogger _logger;
		private readonly IEnsembleRunner _ensembleRunner;

		public InputDevice InputDevice { get; private set; }
		public OutputDevice OutputDevice { get; private set; }

		public void UseDefaults()
		{
			// TODO: Es wird überall -1 als Device übergeben. ist das ok?
			SetInputDevice(InputDevice.InstalledDevices.FirstOrDefault());
			SetOutputDevice(OutputDevice.InstalledDevices.FirstOrDefault());
		}

		public void SetInputDevice(InputDevice device)
		{
			DisconnectInputDevice();
			InputDevice = device;
			if (InputDevice != null)
			{
				InputDevice.NoteOn += OnInputDeviceOnNoteOn;
				InputDevice.NoteOff += OnInputDeviceOnNoteOff;
				InputDevice.ControlChange += OnInputDeviceOnControlChange;
				InputDevice.Open();
				InputDevice.StartReceiving(null);
			}
		}

		public void SetOutputDevice(OutputDevice device)
		{
			DisconnectOutputDevice();
			OutputDevice = device;
			if (OutputDevice != null)
			{
				OutputDevice.Open();
			}
		}

		public override void SendMidiMessage(MidiMessage message)
		{
			base.SendMidiMessage(message);

			if (OutputDevice == null)
				return;

			if (message is NoteOnMessage)
			{
				var noteOnMessage = message as NoteOnMessage;
				OutputDevice.SendNoteOn(
					(Channel)noteOnMessage.Channel.Value, (Pitch)noteOnMessage.Pitch.Value, noteOnMessage.Velocity);
			}
			else if (message is NoteOffMessage)
			{
				var noteOffMessage = message as NoteOffMessage;
				OutputDevice.SendNoteOff(
					(Channel)noteOffMessage.Channel.Value, (Pitch)noteOffMessage.Pitch.Value, noteOffMessage.Velocity);
			}
			else if (message is ControlChangeMessage)
			{
				var controlChangeMessage = message as ControlChangeMessage;
				OutputDevice.SendControlChange(
					(Channel)controlChangeMessage.Channel.Value, (Control)controlChangeMessage.Control, controlChangeMessage.Value);
			}
			else
			{
				throw new Exception("Could not send midi message - message unknown: " + message);
			}
		}

		private void OnInputDeviceOnNoteOn(MidiDotNet.NoteOnMessage msg)
		{
			DispatchMidiMessage(() => new NoteOnMessage((int)msg.Channel, (int)msg.Pitch, (byte)msg.Velocity, -1, 0));
		}

		private void OnInputDeviceOnNoteOff(MidiDotNet.NoteOffMessage msg)
		{
			DispatchMidiMessage(() => new NoteOffMessage((int)msg.Channel, (int)msg.Pitch, (byte)msg.Velocity, -1, 0));
		}

		private void OnInputDeviceOnControlChange(MidiDotNet.ControlChangeMessage msg)
		{
			DispatchMidiMessage(() => new ControlChangeMessage((int)msg.Channel, (byte)msg.Control, (byte)msg.Value, -1, 0));
		}

		private void DispatchMidiMessage(Func<MidiMessage> createMessage)
		{
			// im createMessage kann was schief gehen mit den ganzen casts, deshalb das try-catch
			InvokeAndStopOnError(() =>
				{
					if (OnMessage == null)
						return;
					OnMessage(createMessage());
				});
		}

		// TODO: Diese Methode gibts öfter (vom Prinzip) - vereinheitlichen
		private void InvokeAndStopOnError(Action del)
		{
			try
			{
				del();
			}
			catch (Exception e)
			{
				_logger.Log("Error in Midi Callback handling: " + e);
				_ensembleRunner.ReportRuntimeError(e);
			}
		}

		public void Dispose()
		{
			DisconnectInputDevice();
			DisconnectOutputDevice();
		}

		private void DisconnectInputDevice()
		{
			if (InputDevice == null)
				return;

			InputDevice.StopReceiving();
			InputDevice.Close();
			InputDevice.RemoveAllEventHandlers();
			InputDevice = null;
		}

		private void DisconnectOutputDevice()
		{
			if (OutputDevice == null)
				return;

			OutputDevice.Close();
			OutputDevice = null;
		}
	}
}